import logging
import re
import sys
import json
import zipfile
import os
from io import BytesIO

from functools import wraps
from typing import Iterable
# 第三方库

import dateutil
import dateutil.parser
import requests


class Garmin():
    SSO_URL = "https://sso.garmin.cn"
    SSO_LOGIN_URL = "https://sso.garmin.cn/sso/signin"
    """Garmin Connect SSO URL"""
    CONNECT_MODERN_URL = "https://connect.garmin.cn/modern/"
    CONNECT_LEGACY_SESSION_URL = "https://connect.garmin.cn/legacy/session"
    CONNECT_MODERN_ACTIVITIES = "https://connect.garmin.cn/modern/proxy/activitylist-service/activities/search/activities"
    CONNECT_MODERN_DOWNLOAD_FILES = "https://connect.garmin.cn/modern/proxy/download-service/files/activity/{}"

class GarminEn_US():
    SSO_URL = "https://sso.garmin.com/"
    SSO_LOGIN_URL = "https://sso.garmin.com/sso/signin"
    """Garmin Connect SSO URL"""
    CONNECT_MODERN_URL = "https://connect.garmin.com/modern/"
    CONNECT_LEGACY_SESSION_URL = "https://connect.garmin.com/legacy/session"
    CONNECT_MODERN_ACTIVITIES = "https://connect.garmin.com/modern/proxy/activitylist-service/activities/search/activities"


log = logging.getLogger(__name__)
log.setLevel(logging.INFO)
#  reduce logging noise from requests library
logging.getLogger("requests").setLevel(logging.DEBUG)


def require_session(client_function):
    """Decorator that is used to annotate :class:`GarminClient`
    methods that need an authenticated session before being called.
    装饰器, 用于注释需要经过身份验证的会话的GarminClient方法。
    :param client_function:
    :return:
    """

    @wraps(client_function)
    def check_session(*args, **kwargs):
        client_object = args[0]
        if not client_object.session:
            raise Exception("")
        return client_function(*args, **kwargs)

    return check_session


class GarminClient(object):
    """A client class used to authenticate with Garmin Connect and
    extract data from the user account.
    """

    def __init__(self, username, password):
        """Initialize a :class: `GarminClient` instance.

        :param username: Garmin user name
        :type username: str
        :param password: Garmin user password
        :type password: str
        """
        self.username = username
        self.password = password
        self.session = None

    def __enter__(self):
        self.connect()
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.disconnect()

    def connect(self):
        self.session = requests.Session()
        self._authenticate()

    def disconnect(self):
        if self.session:
            self.session.close()
            self.session = None

    def _authenticate(self):
        """登录认证
        """
        log.info("authenticating user ...")
        form_data = {
            "username": self.username,
            "password": self.password,
            "embed": "false"
        }
        request_params = {
            "service": Garmin.CONNECT_MODERN_URL
        }
        headers = {
            "origin": Garmin.SSO_URL,
            "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36"
        }
        auth_response = self.session.post(Garmin.SSO_LOGIN_URL, headers=headers, params=request_params, data=form_data)
        log.debug("got auth response: %s", auth_response.text)

        if auth_response.status_code != 200:
            raise ValueError("身份验证失败: 输入的用户名密码无效")
        auth_ticket_url = self._extract_auth_ticket_url(auth_response.text)
        log.debug(f'auth ticket url: {auth_ticket_url}')

        log.info("claiming auth ticket...")
        # 登录后使用此 session 对象来发起请求
        response = self.session.get(auth_ticket_url)
        if response.status_code != 200:
            raise RuntimeError(
                f"auth failure: failed to claim auth ticket: {auth_ticket_url} : {response.status_code} : {response.text}")
        # 似乎要使用旧 API
        # r = self.session.get(Garmin.CONNECT_LEGACY_SESSION_URL)
        # print(r.status_code) # 404

    @staticmethod
    def _extract_auth_ticket_url(auth_response):
        """从身份验证表单提交的响应中提取身份验证票据URL。认证票URL的典型形式是:
                  https://connect.garmin.cn/modern?ticket=ST-0123456-aBCDefgh1iJkLmN5opQ9R-cas
        :param auth_response: HTML response from an auth form submission.
        """
        match = re.search(r'response_url\s*=\s*"(https:[^"]+)"', auth_response)
        if not match:
            raise RuntimeError("认证失败:无法提取认证票URL。您是否提供了正确的用户名/密码")
        auth_ticket_url = match.group(1).replace("\\", "")
        return auth_ticket_url

    @require_session
    def list_activities(self):
        """返回登录用户存储的所有活动ID及其开始时间戳。
        :returns: The full list of activity identifiers (along with their starting timestamps).
        :rtype: tuples of (int, datetime)
        # :param start_time: 开始时间
        :return: ids
        """
        ids = []
        batch_size = 100
        # fetch in batches since the API doesn't allow more than certain 由于API不允许超出一定数量，因此批量获取
        # number of activities to be retrieved on every invocation 每次调用要检索的活动数
        for start_index in range(0, sys.maxsize, batch_size):
            next_batch = self._fetch_activity_ids_and_ts(start_index, batch_size)
            if not next_batch:
                break
            ids.extend(next_batch)
        return ids

    @require_session
    def _fetch_activity_ids_and_ts(self, start_index, max_limit=20):
        """批量获取 active 的 ids 和 ts
        返回一个从给定索引开始的活动 id 序列(以及它们的起始时间戳) ，索引0是用户最近注册的活动。
        """
        response = self.session.get(
            Garmin.CONNECT_MODERN_ACTIVITIES, params={"start": start_index, "limit": max_limit}
        )
        if response.status_code != 200:
            raise Exception(u"failed to fetch activities {} to {} types: {}\n{}"
                            .format(start_index, (start_index + max_limit - 1), response.status_code, response.text))
        activities = json.loads(response.text)
        if not activities:
            return []

        entries = []
        for activity in activities:
            id = int(activity["activityId"])
            timestamp_local = dateutil.parser.parse(activity["startTimeLocal"])
            timestamp_utc = dateutil.parser.parse(activity["startTimeGMT"])
            # 转换为时间戳
            timestamp_utc = timestamp_utc.replace(tzinfo=dateutil.tz.tzutc())
            entries.append((id, timestamp_utc))
        log.debug(f'got {len(entries)} activities.')
        return entries

    @require_session
    def get_activity_summary(self, activity_id):
        """获取活动概要
        :param activity_id:
        :return:
        """
        response = self.session.get(
            "https://connect.garmin.cn/modern/proxy/activity-service/activity/{}".format(activity_id))
        if response.status_code != 200:
            log.error(u"failed to fetch json summary for activity {}: {}\n{}".format(
                activity_id, response.status_code, response.text))
            raise Exception(u"failed to fetch json summary for activity {}: {}\n{}".format(
                activity_id, response.status_code, response.text))
        return json.loads(response.text)

    @require_session
    def get_activity_details(self, activity_id):
        pass

    @require_session
    def get_activity_gpx(self, activity_id):
        pass

    @require_session
    def get_activity_tcx(self, activity_id):
        pass

    @require_session
    def get_original_activity(self, activity_id):
        """获取活动原始值
        佳明设备导入才有原始值(如手动输入), 原始值中有fit文件
        :param activity_id: Activitiy ID
        :type activity_id: int
        :returns: A tuple of the file type(e.g. 'fit', 'tcx', 'gpx') and its contents, or :obj:`(None, None) if no file is found.
        :rtype: (str, str)
        """
        response = self.session.get(Garmin.CONNECT_MODERN_DOWNLOAD_FILES.format(activity_id))
        if response.status_code in [404, 500]:
            return None,None
        if response.status_code != 200:
            raise Exception(u"failed to get original activity file for {}: {}\n{}".format(activity_id, response.status_code, response.text))
        zip_file = zipfile.ZipFile(BytesIO(response.content), mode='r')
        for path in zip_file.namelist():
            # file_name, ext_name
            fn, ext = os.path.splitext(path)
            if fn.startswith(str(activity_id)):
                return ext[1:], zip_file.open(path).read()
        return None, None


    def get_activity_fit(self, activity_id):
        """获取活动 fit 文件
        :param activity_id:
        :return:
        """
        fmt, original_file  = self.get_original_activity(activity_id)
        return original_file if fmt == 'fit' else None
